package org.net5ijy.cloud.plugin.feign.core;

import org.net5ijy.cloud.plugin.feign.core.model.FeignClientClass;
import org.net5ijy.cloud.plugin.feign.core.model.FeignMethod;
import org.net5ijy.cloud.plugin.feign.core.model.FeignMethodArgument;
import org.net5ijy.cloud.plugin.feign.core.util.ClazzUtil;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.net5ijy.cloud.plugin.feign.core.util.SpringControllerUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * FeignClientScanner
 *
 * @author XGF
 * @date 2020/11/20 14:30
 */
public class FeignClientScanner {

    private Set<String> modelClassNames = new HashSet<>();

    private String modelScanPath;
    private List<String> sourcePaths;

    public FeignClientScanner(String modelScanPath, List<String> compileSourceRoots) {
        this.modelScanPath = modelScanPath;
        this.sourcePaths = compileSourceRoots;
    }

    public List<FeignClientClass> scan(String scanPackage) {

        List<String> clazzList = ClazzUtil.getClazzName(scanPackage, true);
        List<FeignClientClass> list = new ArrayList<>();
        for (String clazzName : clazzList) {

            // 反射获取controller类
            try {
                Class<?> clazz = Class.forName(clazzName);

                // 判断是否是一个controller
                Controller controller = clazz.getAnnotation(Controller.class);

                if (controller == null) {
                    RestController restController = clazz.getAnnotation(RestController.class);
                    if (restController == null) {
                        continue;
                    }
                }
                FeignClientClass feignClientClass = resolveFeignClientClass(clazz);

                if (feignClientClass == null) {

                } else {
                    list.add(feignClientClass);
                }
            } catch (Throwable e) {

            }
        }

        return list;
    }

    private FeignClientClass resolveFeignClientClass(Class<?> clazz) {

        RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);

        // 创建一个FeignClientClass
        FeignClientClass feignClientClass = new FeignClientClass();

        // 获取类名
        String clazzSimpleName = clazz.getSimpleName();

        // 获取feign接口名
        String feignName = clazzSimpleName.replaceAll("Controller", "");
        // 设置feign类名
        feignClientClass.setFeignClassName(feignName + "Feign");

        // 获取url
        String url = requestMapping == null ? "" : SpringControllerUtil.resolveUrlFromRequestMapping(requestMapping);


        // base url
        feignClientClass.setUrl(url);

        // 获取所有方法
        Method[] methods = clazz.getMethods();

        // 保存这个feign下面的所有接口方法
        List<FeignMethod> feignMethodList = new ArrayList<>();

        for (Method method : methods) {
            FeignMethod feignMethod = resolveFeignMethodFromJavaMethod(method);
            if (feignMethod != null) {
                feignMethodList.add(feignMethod);
            }
        }

        feignClientClass.setMethods(feignMethodList);

        return feignClientClass;
    }

    private FeignMethod resolveFeignMethodFromJavaMethod(Method method) {
        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);

        String url = null;
        String httpMethod = null;

        if (requestMapping == null) {
            if (method.getAnnotation(GetMapping.class) != null) {
                GetMapping getMapping = method.getAnnotation(GetMapping.class);
                url = SpringControllerUtil.resolveUrlFromGetMapping(getMapping);
                httpMethod = "GET";
            } else if (method.getAnnotation(PostMapping.class) != null) {
                PostMapping postMapping = method.getAnnotation(PostMapping.class);
                url = SpringControllerUtil.resolveUrlFromPostMapping(postMapping);
                httpMethod = "POST";
            } else if (method.getAnnotation(PutMapping.class) != null) {
                PutMapping putMapping = method.getAnnotation(PutMapping.class);
                url = SpringControllerUtil.resolveUrlFromPutMapping(putMapping);
                httpMethod = "PUT";
            } else if (method.getAnnotation(DeleteMapping.class) != null) {
                DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
                url = SpringControllerUtil.resolveUrlFromDeleteMapping(deleteMapping);
                httpMethod = "DELETE";
            }
        } else {
            url = SpringControllerUtil.resolveUrlFromRequestMapping(requestMapping);
            httpMethod = SpringControllerUtil.resolveHttpMethodFromRequestMapping(requestMapping);
        }

        if (url == null) {
            return null;
        }

        FeignMethod feignMethod = new FeignMethod();
        feignMethod.setUrl(url);
        feignMethod.setHttpMethod(httpMethod);

        feignMethod.setName(method.getName());

        Type genericReturnType = method.getGenericReturnType();

        feignMethod.setReturnType(
                genericReturnType.getTypeName().replaceAll("([a-z0-9]+\\.)*", ""));

        try {
            if (genericReturnType instanceof ParameterizedType) {
                getModelClassFromType(
                        (ParameterizedType) method.getGenericReturnType(), modelClassNames);
            } else {
                String name = genericReturnType.getTypeName();
                if (name.startsWith(modelScanPath)) {
                    modelClassNames.add(name);
                    getModelClassFromClassName(name, modelClassNames);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        // 解析方法参数
        List<FeignMethodArgument> arguments = new ArrayList<>();

        Parameter[] parameters = method.getParameters();

        for (Parameter parameter : parameters) {
            FeignMethodArgument feignMethodArgument = resolveFeignMethodArgumentFromParameter(parameter);

            if (parameter.isAnnotationPresent(RequestBody.class)){
                httpMethod="POST";
                feignMethod.setHttpMethod(httpMethod);
            }
            arguments.add(feignMethodArgument);
        }

        feignMethod.setArguments(arguments);

        return feignMethod;
    }

    private FeignMethodArgument resolveFeignMethodArgumentFromParameter(Parameter parameter) {

        FeignMethodArgument argument = new FeignMethodArgument();

        String name = parameter.getName();
        Class<?> type = parameter.getType();
        Type parameterizedType = parameter.getParameterizedType();

        Annotation[] annotations = parameter.getAnnotations();
        List<String> as = new ArrayList<>();
        for (Annotation annotation : annotations) {
            if (annotation instanceof RequestBody
                    || annotation instanceof PathVariable
                    || annotation instanceof RequestParam) {
                as.add(annotation.annotationType().getSimpleName());
            }
        }

        argument.setArgName(name);
        argument.setArgType(parameterizedType.getTypeName().replaceAll("([a-z0-9]+\\.)*", ""));
        argument.setAnnotations(as);

        try {
            if (parameterizedType instanceof ParameterizedType) {
                getModelClassFromType(
                        (ParameterizedType) parameterizedType, modelClassNames);
            } else {
                String name1 = parameterizedType.getTypeName();
                if (name1.startsWith(modelScanPath)) {
                    getModelClassFromClassName(name1, modelClassNames);
                    modelClassNames.add(name1);
                }
            }
        } catch (ClassNotFoundException e) {

        }

        String argumentClassName = type.getName();
        if (argumentClassName.startsWith(modelScanPath)) {
            modelClassNames.add(argumentClassName);
        }

        return argument;
    }

    private void getModelClassFromType(ParameterizedType type, Set<String> list)
            throws ClassNotFoundException {
        Type rawType = type.getRawType();
        Class<? extends Type> aClass = rawType.getClass();
        if (notClass(aClass)) {
            return;
        }
        String typeName = rawType.getTypeName();
        if (typeName.startsWith(modelScanPath)) {
            list.add(typeName);
        }
        Type[] actualTypeArguments = type.getActualTypeArguments();
        for (Type actualTypeArgument : actualTypeArguments) {
            if (actualTypeArgument instanceof ParameterizedType) {
                getModelClassFromType((ParameterizedType) actualTypeArgument, list);
            } else {
                String name = actualTypeArgument.getTypeName();
                if (name.startsWith(modelScanPath) && !list.contains(name)) {
                    list.add(name);
                    getModelClassFromClassName(name, list);
                }
            }
        }
    }

    private void getModelClassFromClassName(String className, Set<String> list)
            throws ClassNotFoundException {
        Class<?> clazz = Class.forName(className);
        if (notClass(clazz)) {
            return;
        }
        if (list.contains(className)) {
            return;
        }
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            Type type = field.getGenericType();
            if (type instanceof ParameterizedType) {
                getModelClassFromType((ParameterizedType) type, list);
            } else {
                String name = type.getTypeName();
                if (name.startsWith(modelScanPath) && !list.contains(name)) {
                    list.add(name);
                    getModelClassFromClassName(name, list);
                }
            }
        }
    }

    private boolean notClass(Class clazz) {
        return clazz.isAnnotation() ||
                clazz.isAnonymousClass() ||
                clazz.isArray() ||
                clazz.isEnum() ||
                clazz.isInterface();
    }

    public Set<String> getModelClassNames() {
        return modelClassNames;
    }
}
